sub _make_multiport_groupings
{
    my @ret = (shift);

    while (@_)
    {
	# protocol is the same, and less than 15 ports
	if ($_[0][0] eq $ret[-1][0]
	    and 1 + ($_[0][1] =~ y/:,/:,/) + 1 + ($ret[-1][1] =~ y/:,/:,/) <= 15)
        {
	    $ret[-1][1] .= ",$_[0][1]";
	    shift;
            next;
        }

        push @ret, shift;
    }

    @ret;
}

sub _expand
{
    my ($action, $src, $dst) = @_;

    $dst = { any=>$dst }
	unless ref $dst eq 'HASH';

    my @ret;

    while (my ($addr, $ports) = each %{ $dst })
    {
	my @ports = _make_multiport_groupings
	    sort { $a->[0] cmp $b->[0] || $a->[1] <=> $b->[1] }
	    map { $_->[1] = getservbyname($_->[1], $_->[0]) || $_->[1] unless $_->[1] =~ /^\d+$/; $_ }
	    map [ s/:(udp|tcp)$// ? $1 : 'tcp', $_ ],
	    comma_or_arrayref($ports);

	foreach (@ports)
	{
	    my ($proto, $port) = @$_;

	    push @ret, {
		protocol => $proto,
		$src =~ /\b(any|all)\b/ ? () : (source => $src),
		$addr =~ /\b(any|all)\b/ ? () : (dest => $addr),
		$port =~ /\b(any|all)\b/ ? () : ($port =~ y/:,/:,/ ? (dports => $port) : (dport => $port)),
		action   => $action,
	    };
	}
    }

    @ret;
}

my %action_map = (allow => 'ACCEPT', deny => 'DROP');

my %firewall = (
    nat => { PREROUTING=>[], POSTROUTING=>[], OUTPUT=>[] },
    filter => {
	INPUT   => [
	    "-p icmp -j ACCEPT",
	    "-i lo -j ACCEPT",
	    "-m state --state ESTABLISHED,RELATED -j ACCEPT",
	],
	FORWARD => [],
	OUTPUT  => [],
     },
);

foreach my $tag (map +("firewall${_}_block", "firewall${_}_allow"), "", "_input", "_forward", "_output")
{
    my ($chain, $action) = $tag =~ /^firewall(?:_(input|forward|output))?_(allow|block)$/
	or die;
    $chain ||= 'INPUT';
    $chain = uc $chain;
    $action = $action_map{ $action };

    my @rules = fetch_all($tag)
	or next;

    foreach my $rule (@rules)
    {
	unless (ref $rule eq 'HASH')
	{
	    warn "value for $tag must be a HASH\n";
	    next;
	}

	push @{ $firewall{filter}{$chain} },
	    map _expand($action, $_, $rule->{$_}),
	    sort keys %$rule;
    }
}

my $conf = '';

foreach my $table (sort keys %firewall)
{
    $conf .= "*$table\n";
    $conf .= ":$_ ACCEPT [0:0]\n"
	foreach sort keys %{ $firewall{$table} };

    foreach my $chain (sort keys %{ $firewall{$table} })
    {
	$conf .= "-A $chain $_\n"
	    foreach fetchall_array(lc "firewall_${chain}_prepend");

	foreach my $rule (@{ $firewall{$table}{$chain} })
	{
	    unless (ref $rule eq 'HASH')
	    {
		$conf .= "-A $chain $rule";
		$conf =~ s/\n?$/\n/;
		next;
	    }

	    $conf .= "-A $chain";
	    $conf .= " -m multiport" if $rule->{sports} || $rule->{dports};
	    $conf .= " -p $rule->{protocol}" if $rule->{protocol};
	    $conf .= " -s $rule->{source}" if $rule->{source};
	    $conf .= " --sport $rule->{sport}" if $rule->{sport};
	    $conf .= " --sports $rule->{sports}" if $rule->{sports};
	    $conf .= " -d $rule->{dest}" if $rule->{dest};
	    $conf .= " --dport $rule->{dport}" if $rule->{dport};
	    $conf .= " --dports $rule->{dports}" if $rule->{dports};
	    $conf .= " -j $rule->{action}";
	    $conf .= "\n";
	}

	$conf .= "-A $chain $_\n"
	    foreach fetchall_array(lc "firewall_${chain}_append");

	$conf .= "-A $chain -j REJECT --reject-with icmp-admin-prohibited\n"
	    if $table eq 'filter' and $chain =~ /^(INPUT|FORWARD)$/;
    }

    $conf .= "COMMIT\n";
}

$conf .= "$_\n"
    foreach fetchall_array("firewall_append");

text_install('/etc/sysconfig/iptables', $conf, "service iptables start");
